package jeeobserver.server.database;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import jeeobserver.server.database.DatabaseFile.DatabaseEntity;
import jeeobserver.server.database.DatabaseFile.DatabaseFileFixedRecord;
import jeeobserver.server.RequestParameter;
import jeeobserver.server.database.DatabaseFile.HeaderEntity;
import jeeobserver.utilities.DatabaseUtilities;

class DatabaseFileStatistics<T extends DatabaseFileStatistics.DatabaseStatisticsEntity> extends DatabaseFile.DatabaseFileFixedRecord {

	private static final int PAGE_SIZE = 100;

	private static final int OPTIMIZATION_THRESHOLD = 10000;

	private static final int BULK_SIZE = 1000;

	private Map<Date, Set<Long>> indexTime;

	private Map<Integer, Set<Long>> indexElement;

	private final String path;

	public DatabaseFileStatistics(String path, Class type, int recordLenght) throws IOException {
		super(path + ".dat", type, recordLenght);

		this.path = path;

		//Load into memory time and element index
		this.calculateIndexes();
	}

	public synchronized void persist(Set<DatabaseStatisticsEntity> entities) throws IOException {

		long position = this.file.length();

		this.file.seek(position);

		this.write(entities);

		for (DatabaseStatisticsEntity entity : entities) {
			this.updateIndexes(entity, position);
			position = position + entity.getSize();
		}

		if (this.getHeader().getUnoptimizedRows() > OPTIMIZATION_THRESHOLD) {
			System.out.println("OPTIMIZEEEEEEE! : " + this.getHeader().getUnoptimizedRows());
			this.optimize();
		}
	}

	@Override
	public synchronized void write(Set entities) throws IOException {

		super.write(entities);

		//Update unoptimized rows to optimize
		StatisticsHeaderEntity header = this.getHeader();

		header.setUnoptimizedRows(header.getUnoptimizedRows() + entities.size());

		this.updateHeader(header);
	}

	private void calculateIndexes() throws IOException {

		this.indexTime = new HashMap<Date, Set<Long>>();
		this.indexElement = new HashMap<Integer, Set<Long>>();

		DatabaseStatisticsEntity statisticEntity;

		this.reset();

		while ((statisticEntity = (DatabaseStatisticsEntity) this.read()) != null) {
			if (statisticEntity.getStatus() != DatabaseStatisticsEntity.STATUS_DELETED) {
				updateIndexes(statisticEntity, this.file.getFilePointer() - statisticEntity.getSize());
			}
		}

	}

	private synchronized void updateIndexes(DatabaseStatisticsEntity statisticEntity, long position) throws IOException {
		long page = (position - HeaderEntity.BYTES_SIZE) / (statisticEntity.getSize() * PAGE_SIZE);

		//Time index (sampled by hour)
		Date indexDate = DatabaseUtilities.calculateStatisticTimestamp(statisticEntity.getDate(), RequestParameter.SAMPLING_PERIOD_HOUR);

		if (!this.indexTime.containsKey(indexDate)) {
			this.indexTime.put(indexDate, new TreeSet<Long>());
		}
		this.indexTime.get(indexDate).add(page);

		//Element index
		if (!this.indexElement.containsKey(statisticEntity.getElementId())) {
			this.indexElement.put(statisticEntity.getElementId(), new TreeSet<Long>());
		}
		this.indexElement.get(statisticEntity.getElementId()).add(page);
	}

	public synchronized int delete(Set<Integer> elementIds, Date dateFrom, Date dateTo) throws IOException {
		int result = 0;

		this.reset();

		//Load indexPages
		Set<Long> indexPages = this.getIndexPages(elementIds, dateFrom, dateTo);

		for (long page : indexPages) {

			this.file.seek(HeaderEntity.BYTES_SIZE + (page * (PAGE_SIZE * this.recordLenght)));

			int count = 0;
			DatabaseStatisticsEntity entity;
			while (((entity = (DatabaseStatisticsEntity) this.read()) != null) && count < PAGE_SIZE) {
				if (entity != null) {

					boolean filterPassed = true;
					if (entity.getStatus() != DatabaseStatisticsEntity.STATUS_ACTIVE) {
						filterPassed = false;
					} else if (!elementIds.contains(entity.getElementId())) {
						filterPassed = true;
					} else if (dateFrom != null && entity.getDate().before(dateFrom)) {
						filterPassed = false;
					} else if (dateTo != null && entity.getDate().after(dateTo)) {
						filterPassed = false;
					}

					if (filterPassed) {
						entity.setStatus(DatabaseStatisticsEntity.STATUS_DELETED);
						this.writePervious(entity);
					}
				}


				count = count + 1;
			}
		}

		return result;

	}

	public synchronized <T> List<T> search(Set<Integer> elementIds, Date dateFrom, Date dateTo) throws IOException {

		this.reset();

		//byte bytes[] = new byte[fileStatistics.recordLenght];

		List<T> results = new ArrayList<T>();

		//Load indexPages
		Set<Long> indexPages = this.getIndexPages(elementIds, dateFrom, dateTo);

		DatabaseStatisticsEntity entity;
		for (long page : indexPages) {

			this.file.seek(HeaderEntity.BYTES_SIZE + (page * (PAGE_SIZE * this.recordLenght)));

			int count = 0;
			while (((entity = (DatabaseStatisticsEntity) this.read()) != null) && count < PAGE_SIZE) {
				if (entity != null) {

					boolean filterPassed = true;

					if (entity.getStatus() != DatabaseStatisticsEntity.STATUS_ACTIVE) {
						filterPassed = false;
					} else if (!elementIds.contains(entity.getElementId())) {
						filterPassed = false;
					} else if (dateFrom != null && entity.getDate().before(dateFrom)) {
						filterPassed = false;
					} else if (dateTo != null && entity.getDate().after(dateTo)) {
						filterPassed = false;
					}

					if (filterPassed) {
						results.add((T) entity);
					}
				}

				count = count + 1;
			}
		}

		return results;
	}

	private synchronized Set<Long> getIndexPages(Set<Integer> elementIds, Date dateFrom, Date dateTo) {

		//Load indexPages
		Set<Long> indexTimePages = new HashSet<Long>();
		Set<Long> indexElementPages = new HashSet<Long>();

		//Load time index
		for (Date id : indexTime.keySet()) {
			if ((dateFrom == null || id.getTime() >= dateFrom.getTime()) && (dateTo == null || id.getTime() < dateTo.getTime())) {
				indexTimePages.addAll(indexTime.get(id));
			}
		}

		//Load element index
		for (int id : indexElement.keySet()) {
			if (elementIds.contains(id)) {
				indexElementPages.addAll(indexElement.get(id));
			}
		}

		//Intersect time and element indexes
		indexTimePages.retainAll(indexElementPages);

		return indexTimePages;
	}

	private synchronized void optimize() throws IOException {

		//1 Check for duplicates and remove deleted rows

		DatabaseStatisticsEntity statisticEntity;

		Map<Integer, OptimizedRow> duplicatesMap = new HashMap<Integer, OptimizedRow>();

		this.reset();

		while ((statisticEntity = (DatabaseStatisticsEntity) this.read()) != null) {
			if (statisticEntity.getStatus() != DatabaseStatisticsEntity.STATUS_DELETED) {

				if (!duplicatesMap.containsKey(statisticEntity.getId())) {
					duplicatesMap.put(statisticEntity.getId(), new OptimizedRow(statisticEntity.getDate(), statisticEntity.getElementId(), this.file.getFilePointer() - statisticEntity.getSize()));
				} else {
					duplicatesMap.get(statisticEntity.getId()).addPosition(this.file.getFilePointer() - statisticEntity.getSize());
				}
			}
		}


		//2 Create working file

		DatabaseFileFixedRecord fileWorking = new DatabaseFileFixedRecord<DatabaseStatisticsEntity>(this.path + ".working", this.type, this.recordLenght);

		//If working file already exists append
		fileWorking.file.seek(fileWorking.file.length());

		Set<DatabaseStatisticsEntity> buffer = new HashSet<DatabaseStatisticsEntity>();

		for (OptimizedRow row : new TreeSet<OptimizedRow>(duplicatesMap.values())) {

			statisticEntity = null;

			for (long position : row.getPositions()) {

				this.file.seek(position);
				if (statisticEntity == null) {
					statisticEntity = (DatabaseStatisticsEntity) this.read();
				} else {
					statisticEntity.add((DatabaseStatisticsEntity) this.read());
				}
			}

			buffer.add(statisticEntity);

			if (buffer.size() >= BULK_SIZE) {
				fileWorking.write(buffer);
				buffer.clear();
			}
		}

		fileWorking.write(buffer);
		buffer.clear();



		//3 Update statistics file

		//If working file already exists append
		this.file.setLength(fileWorking.file.length());
		this.reset();

		while ((statisticEntity = (DatabaseStatisticsEntity) fileWorking.read()) != null) {
			buffer.add(statisticEntity);

			if (buffer.size() >= BULK_SIZE) {
				this.write(buffer);
				buffer.clear();
			}
		}

		this.write(buffer);

		fileWorking.delete();


		//4 Update unoptimized rows to optimize
		StatisticsHeaderEntity header = this.getHeader();

		header.setUnoptimizedRows(0);

		this.updateHeader(header);


		//5 recalculate indexes

		this.calculateIndexes();
	}

	@Override
	public synchronized void clear() throws IOException {
		super.clear();
		this.indexElement.clear();
		this.indexTime.clear();
	}

	public synchronized StatisticsHeaderEntity getHeader() throws IOException {
		this.file.seek(0);

		byte[] header = new byte[HeaderEntity.BYTES_SIZE];
		this.file.read(header);

		return new StatisticsHeaderEntity(ByteBuffer.wrap(header));
	}

	public synchronized void updateHeader(StatisticsHeaderEntity headerEntity) throws IOException {
		this.file.seek(0);
		this.file.write(headerEntity.getBytes());
	}

	public interface DatabaseStatisticsEntity extends DatabaseEntity {

		public static final byte STATUS_ACTIVE = 0;

		public static final byte STATUS_DELETED = 1;

		public abstract Date getDate();

		public abstract int getId();

		public abstract int getElementId();

		public abstract byte getStatus();

		public abstract void setStatus(byte status);

		public abstract void add(DatabaseStatisticsEntity entity);
	}

	public abstract static class DatabaseStatisticsEntityNEW implements DatabaseEntity {

		public static final byte STATUS_DELETED = 1;

		public static final byte STATUS_ACTIVE = 0;

		protected byte status;

		public DatabaseStatisticsEntityNEW(byte status) {
			this.status = status;
		}

		public byte getStatus() {
			return status;
		}

		public void setStatus(byte status) {
			this.status = status;
		}

		public abstract Date getDate();

		public abstract int getId();

		public abstract int getElementId();

		public abstract void add(DatabaseStatisticsEntity entity);
	}

	public static final class StatisticsHeaderEntity extends HeaderEntity implements DatabaseEntity {

		private static final int BYTES_OFFSET = 128;

		private byte samplingPeriod;

		private long unoptimizedRows;

		public StatisticsHeaderEntity(ByteBuffer buffer) {
			super(buffer);

			buffer.position(BYTES_OFFSET);

			this.samplingPeriod = buffer.get();

			this.unoptimizedRows = buffer.getLong();
		}

		public StatisticsHeaderEntity(byte[] bytes) {
			this(ByteBuffer.wrap(bytes));
		}

		public StatisticsHeaderEntity(byte samplingPeriod) {
			super();
			this.samplingPeriod = samplingPeriod;

			this.unoptimizedRows = 0;
		}

		@Override
		public byte[] getBytes() {

			ByteBuffer buffer = ByteBuffer.allocate(BYTES_SIZE);

			buffer.put(super.getBytes());

			buffer.position(BYTES_OFFSET);

			buffer.put(this.samplingPeriod);

			buffer.putLong(this.unoptimizedRows);

			return buffer.array();
		}

		public byte getSamplingPeriod() {
			return samplingPeriod;
		}

		public void setSamplingPeriod(byte samplingPeriod) {
			this.samplingPeriod = samplingPeriod;
		}

		public long getUnoptimizedRows() {
			return unoptimizedRows;
		}

		public void setUnoptimizedRows(long unoptimizedRows) {
			this.unoptimizedRows = unoptimizedRows;
		}
	}

	private static final class OptimizedRow implements Comparable<OptimizedRow> {

		private Date date;

		private int id;

		private Set<Long> positions;

		public OptimizedRow(Date date, int id, long position) {
			this.date = date;
			this.id = id;
			this.positions = new HashSet<Long>();
			this.positions.add(position);
		}

		public void addPosition(long position) {
			this.positions.add(position);
		}

		public Date getDate() {
			return date;
		}

		public int getId() {
			return id;
		}

		public Set<Long> getPositions() {
			return positions;
		}

		@Override
		public int hashCode() {
			int hash = 5;
			hash = 79 * hash + (this.date != null ? this.date.hashCode() : 0);
			hash = 79 * hash + this.id;
			return hash;
		}

		@Override
		public boolean equals(Object obj) {
			if (obj == null) {
				return false;
			}
			if (getClass() != obj.getClass()) {
				return false;
			}
			final OptimizedRow other = (OptimizedRow) obj;
			if (this.date != other.date && (this.date == null || !this.date.equals(other.date))) {
				return false;
			}
			if (this.id != other.id) {
				return false;
			}
			return true;
		}

		public int compareTo(OptimizedRow o) {
			int result = this.getDate().compareTo(o.getDate());

			if (result == 0) {
				result = new Integer(this.getId()).compareTo(new Integer(o.getId()));
			}
			return result;
		}
	}
}
